package com.ml.redis.lock;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Service;

import java.nio.charset.Charset;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * 获取锁使用的SET_IF_ABSENT命令，释放锁使用的LUA脚本
 * 魔力版
 */
@Service
@Slf4j
public class MmearsDistributedLock {

    public static final Charset CHARSET = Charset.forName("UTF-8");

    public static final String UNLOCK_LUA;

    /**
     * 释放锁脚本，原子操作
     */
    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 获取分布式锁，加等待时间
     *
     * @return
     */
    public boolean tryLockWithWait(String lockKey, String lockVal, long expire, TimeUnit expireTimeUnit, long wait, TimeUnit waitTimeUnit) {
        log.info("tryLockWithWait for {} - {} begin, expire {}, expireTimeUnit {}, wait {}, waitTimeUnit {}", lockKey, lockVal, expire, expireTimeUnit, wait, waitTimeUnit);

        long waitUntil = System.currentTimeMillis() + waitTimeUnit.toMillis(wait);
        while (!tryLock(lockKey, lockVal, expire, expireTimeUnit)) {
            if (System.currentTimeMillis() >= waitUntil) {
                log.info("tryLockWithWait return false for {} - {}", lockKey, lockVal);
                return false;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                log.info("tryLockWithWait interrupted");
            }
        }
        log.info("tryLockWithWait return true for {} - {}", lockKey, lockVal);
        return true;
    }

    /**
     * 获取分布式锁，原子操作
     *
     * @param lockKey
     * @param lockVal        唯一ID, 可以使用UUID.randomUUID().toString();
     * @param expire
     * @param expireTimeUnit
     * @return
     */
    public boolean tryLock(String lockKey, String lockVal, long expire, TimeUnit expireTimeUnit) {
        try {
            log.info("tryLock for {} - {} begin, expire {}, expireTimeUnit {}", lockKey, lockVal, expire, expireTimeUnit);
            RedisCallback<Boolean> callback = (connection) -> {
                return connection.set(lockKey.getBytes(CHARSET),
                        lockVal.getBytes(CHARSET),
                        Expiration.milliseconds(expireTimeUnit.toMillis(expire)),
                        RedisStringCommands.SetOption.SET_IF_ABSENT);
            };

            boolean result = stringRedisTemplate.execute(callback);
            log.info("tryLock return {} for {} - {}", result, lockKey, lockVal);
            return result;
        } catch (Exception e) {
            log.error("RedisDistributedLockServiceImpl.tryLock exception", e);
        }
        log.info("tryLock return default false for {} - {}", lockKey, lockVal);
        return false;
    }

    /**
     * 释放锁
     *
     * @param lockKey
     * @param lockVal 唯一ID
     * @return
     */
    public boolean releaseLock(String lockKey, String lockVal) {
        log.info("releaseLock for {} - {} begin", lockKey, lockVal);
        RedisCallback<Boolean> callback = (connection) -> {
            return connection.eval(UNLOCK_LUA.getBytes(), ReturnType.BOOLEAN, 1, lockKey.getBytes(CHARSET), lockVal.getBytes(CHARSET));
        };
        boolean releaseSuccess = stringRedisTemplate.execute(callback);
        if (!releaseSuccess) {
            try {
                String existVal = getLockVal(lockKey);
                log.warn("RedisDistributedLockServiceImpl.releaseLock for lockKey[{}], lockVal[{}] fail, existVal[{}]", lockKey, lockVal, existVal);
            } catch (Exception e) {
                log.error("releaseLock false and getLockVal exception for " + lockKey + " - " + lockVal, e);
            }
        }
        log.info("releaseLock for {} - {} result {}", lockKey, lockVal, releaseSuccess);
        return releaseSuccess;
    }

    /**
     * 获取lockVal
     *
     * @param lockKey
     * @return
     */
    public String getLockVal(String lockKey) {
        Object object = stringRedisTemplate.opsForValue().get(lockKey);
        return Optional.ofNullable(object).map(String::valueOf).orElse(null);
    }
}